package lab.cadl.lirui.markdown.core.parser

import lab.cadl.lirui.markdown.core.exceptions.MarkdownException
import lab.cadl.lirui.markdown.core.model.*
import lab.cadl.lirui.markdown.core.utils.Escaping
import lab.cadl.lirui.markdown.core.utils.Html5Entities
import java.util.regex.Matcher

object Patterns {
    const val SPECIAL_CHARS = "\n\r\n\\`[!]<&$*_~{"

    val ENTITY_RE = Regex("^&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});", RegexOption.IGNORE_CASE)
    val SPNL_RE = Regex("^ *(?:\n *)?")
    val DESTINATION_BRACES_RE = Regex("""^<[^<> \t\n\\]*>""")
    val LINK_TITLE_RE = Regex("""^'(\\'|[^'])*'|"(\\"|[^"])*|\((\\\(|[^)])*\)""")
    val LINK_LABLE_RE = Regex("""^\[([^\\\[\]]|\\.)*]""")
}

interface InlineProcessor {
    fun parse(lines: List<String>, container: Block)
    fun parse(line: String, container: Block)
}

class InlineProcessorImpl : InlineProcessor {
    override fun parse(line: String, container: Block) {
        OneParseInlineProcessor(line, container).parse()
    }

    override fun parse(lines: List<String>, container: Block) {
        parse(lines.joinToString(""), container)
    }
}

class Bracket(val text: Text, val index: Int, val isImage: Boolean, val previous: Bracket?, var hasPost: Boolean,
              var allowed: Boolean, val maybeFootnote: Boolean = false, val maybePaperRef: Boolean = false) {
    companion object {
        fun link(text: Text, index: Int, previous: Bracket?, maybeFootnote: Boolean = false, maybePaperRef: Boolean = false): Bracket {
            return Bracket(text, index, false, previous, hasPost = false, allowed = true, maybeFootnote = maybeFootnote, maybePaperRef = maybePaperRef)
        }

        fun image(text: Text, index: Int, previous: Bracket?): Bracket {
            return Bracket(text, index, true, previous, hasPost = false, allowed = true)
        }
    }
}

class OneParseInlineProcessor(private val input: String, private val container: Block) {
    private var index = 0
    private var lastBracket: Bracket? = null

    private fun char(): Char? {
        return if (index < input.length) {
            input[index]
        } else {
            null
        }
    }

    private fun nextChar(): Char? {
        return if (index + 1 < input.length) {
            input[index + 1]
        } else {
            null
        }
    }

    private fun appendText(text: String): Text {
        val element = Text(parent = container, content = text)
        container.appendChild(element)
        return element
    }

    fun parse() {
        var moreToParse = true
        while (moreToParse) {
            moreToParse = parseInline()
        }

        if (index < input.length) {
            appendText(input.substring(index))
        }
    }

    private fun parseInline(): Boolean {
        val c = char() ?: return false

        val ok: Boolean = when (c) {
            '\n' -> parseNewLine()
            '\r' -> parseNewLine()
            '\\' -> parseBackslash()
            '`' -> parseBackticks()
            '$' -> parseDollar()
            '*' -> parseStar()
            '_' -> parseUnderline()
            '~' -> parseWave()
            '&' -> parseEntity()
            '[' -> parseOpenBracket()
            '!' -> parseBang()
            ']' -> parseCloseBracket()
            '{' -> parseCurlyBrace()
            else -> parseString()
        }

        if (!ok) {
            appendText(c.toString())
            index += 1
        }

        return true
    }

    private fun parseCurlyBrace(): Boolean {
        return parseBalance({ _, content ->
            if (container.hasVariable(content)) {
                appendText(container.resolveVariable(content))
                true
            } else {
                false
            }
        }, '}', minPrefixLength = 2, maxPrefixLength = 2)
    }

    private fun parseBang(): Boolean {
        val startIndex = index
        index += 1
        if (char() == '[') {
            index += 1

            val text = appendText("![")
            addBracket(Bracket.image(text, startIndex, lastBracket))
        } else {
            appendText("!")
        }

        return true
    }

    private fun skipWhitespace() {
        matches(Patterns.SPNL_RE)
    }

    private fun parseCloseBracket(): Boolean {
        index += 1
        val startIndex = index

        val opener = lastBracket
        if (opener == null) {
            appendText("]")
            return true
        }

        if (!opener.allowed) {
            appendText("]")
            removeLastBracket()
            return true
        }

        var title = input.substring(opener.index + 1, startIndex - 1)

        if (char() == '(') {
            // [foo](/uri "title")

            index += 1
            skipWhitespace()
            val dest = parseLinkDestination()
            skipWhitespace()
            if (input[index - 1].isWhitespace()) {
                title = parseLinkTitle() ?: title
                skipWhitespace()
            }

            if (char() == ')') {
                index += 1

                val urlLink = UrlLink(container, title = title, url = dest)
                mergeChildren(opener.text, urlLink)
            } else {
                index = startIndex
                appendText("]")
            }
        } else if (char() == '[') {
            // maybe [foo][bar] or [foo][]

            val label = parseLinkLabel()
            if (label != null) {
                val rawRef = if (label.isEmpty()) {
                    title
                } else {
                    label
                }

                val ref = Escaping.normalizeReference(rawRef)
                val refLink = RefLink(container, title = title, ref = ref)
                mergeChildren(opener.text, refLink)
            }
        } else {
            when {
                opener.maybeFootnote -> {
                    val footnote = Footnote(parent = container)
                    mergeChildren(opener.text, footnote)

                    // skip ^
                    val text = footnote.children[0] as Text
                    text.content = text.content.substring(1)
                }
                opener.maybePaperRef -> {
                    val paperRef = PaperRef(parent = container, refId = input.substring(opener.index + 2, index - 1))
                    mergeChildren(opener.text, paperRef)
                    // generate ref index
                    container.document!!.resolvePaperRefIndex(paperRef.refId)
                }
                else -> {
                    appendText("]")
                    index = startIndex
                }
            }
        }

        if (index != startIndex && !opener.isImage) {
            var bracket = lastBracket
            while (bracket != null) {
                if (!bracket.isImage && !bracket.maybeFootnote) {
                    bracket.allowed = false
                }

                bracket = bracket.previous
            }
        }

        removeLastBracket()
        return true
    }

    private fun mergeChildren(delimiterElement: Element, newParent: AbstractElement) {
        val i = container.children.indexOf(delimiterElement)
        assert(i >= 0)
        for (child in container.children.subList(i + 1, container.children.size).toList()) {
            newParent.appendChild(child)
        }

        container.removeChild(delimiterElement)
        container.appendChild(newParent)
    }

    private fun parseLinkLabel(): String? {
        val match = matches(Patterns.LINK_LABLE_RE)
        return if (match == null) {
            null
        } else {
            match.group(1) ?: ""
        }
    }

    private fun parseLinkTitle(): String? {
        val match = matches(Patterns.LINK_TITLE_RE)
        return if (match == null) {
            null
        } else {
            return Escaping.unescaping(match.group(1) ?: "")
        }
    }


    private fun parseLinkDestination(): String {
        val match = matches(Patterns.DESTINATION_BRACES_RE)
        return if (match == null) {
            val startIndex = index
            parseLinkDestinationWithBalancedParens()
            Escaping.unescaping(input.substring(startIndex, index))
        } else {
            val linkS = match.group()
            if (linkS.length == 2) {
                ""
            } else {
                Escaping.unescaping(linkS.substring(1, linkS.length - 1))
            }
        }
    }

    private fun parseLinkDestinationWithBalancedParens() {
        var parensCount = 0
        while (true) {
            when (char()) {
                null -> return
                '\\' -> {
                    if (index + 1 < input.length && Escaping.ESCAPABLE_RE.matches(input.substring(index + 1, index + 2))) {
                        // skip next char
                        index += 1
                    }
                }
                '(' -> parensCount += 1
                ')' -> {
                    if (parensCount == 0) {
                        return
                    } else {
                        parensCount -= 1
                    }
                }
                ' ' -> return
                else -> {
                    if (Character.isISOControl(char()!!.toChar())) {
                        return
                    }
                }
            }

            index += 1
        }
    }

    private fun removeLastBracket() {
        lastBracket = lastBracket!!.previous
    }

    private fun parseOpenBracket(): Boolean {
        val startIndex = index
        index += 1

        val text = appendText("[")
        addBracket(Bracket.link(text, startIndex, lastBracket, maybeFootnote = input[index] == '^',
                maybePaperRef = input[index] == '@'))

        return true
    }

    private fun addBracket(bracket: Bracket) {
        if (lastBracket != null) {
            lastBracket!!.hasPost = true
        }

        lastBracket = bracket
    }

    private fun matches(regex: Regex): Matcher? {
        if (index >= input.length) {
            return null
        }

        if (regex.pattern[0] != '^') {
            throw MarkdownException("matches pattern must starts with ^")
        }

        val matcher = regex.toPattern().matcher(input)
        matcher.region(index, input.length)
        val matched = matcher.find()
        return if (matched) {
            index = matcher.end()
            matcher
        } else {
            null
        }
    }

    private fun parseEntity(): Boolean {
        val match = matches(Patterns.ENTITY_RE) ?: return false

        val raw = match.group()
        val content = Html5Entities.entityToString(raw)
        container.appendChild(EntityText(container, content, raw))
        return true
    }

    private fun parseWave(): Boolean {
        return parseBalance({ _, content ->
            container.appendChild(DeleteText(parent = container, content = content))
            true
        }, minPrefixLength = 2)
    }

    private fun parseUnderline(): Boolean {
        return parseBalance({ _, content ->
            container.appendChild(ItalyText(parent = container, content = content))
            true
        }, minPrefixLength = 2)
    }

    private fun parseStar(): Boolean {
        return parseBalance({ _, content ->
            container.appendChild(Emphasis(parent = container, content = content))
            true
        }, minPrefixLength = 2)
    }

    private fun parseDollar(): Boolean {
        return parseBalance({ _, content ->
            container.appendChild(Formula(parent = container, content = content))
            true
        })
    }

    private fun parseBalance(handler: (prefix: String, content: String) -> Boolean, closeChar: Char? = null,
                             minPrefixLength: Int = 1, maxPrefixLength: Int = Int.MAX_VALUE): Boolean {
        val startChar = char()
        val stopChar = closeChar ?: startChar

        var tickEnd = index + 1
        while (tickEnd < input.length && input[tickEnd] == startChar) {
            tickEnd += 1
        }

        val ticks = input.substring(index, tickEnd)

        if (ticks.length < minPrefixLength || ticks.length > maxPrefixLength) {
            return false
        }


        val closeString = stopChar.toString().repeat(ticks.length)
        val closeIndex = input.indexOf(closeString, tickEnd)
        if (closeIndex == -1) {
            return false
        }

        val content = input.substring(tickEnd, closeIndex)
        if (!handler(ticks, content)) {
            return false
        }

        index = closeIndex + ticks.length
        return true
    }

    private fun parseBackticks(): Boolean {
        return parseBalance({ _, content ->
            container.appendChild(Code(parent = container, content = content))
            true
        })
    }

    private fun parseBackslash(): Boolean {
        index += 1
        val nextC = char()
        val nextS = nextC.toString()
        when {
            nextC == '\n' -> {
                container.appendChild(HardLineBreak(parent = container))
                index += 1
            }
            Escaping.ESCAPABLE_RE.matches(nextS) -> {
                appendText(nextS)
                index += 1
            }
            else -> appendText("\\")
        }

        return true
    }

    private fun parseString(): Boolean {
        var currentIndex = index
        while (currentIndex < input.length && input[currentIndex] !in Patterns.SPECIAL_CHARS) {
            currentIndex += 1
        }

        return if (currentIndex == index) {
            false
        } else {
            appendText(input.substring(index, currentIndex))
            index = currentIndex
            true
        }
    }

    private fun parseNewLine(): Boolean {
        container.appendChild(HardLineBreak(parent = container))
        index += 1

        // for \r\n
        if (char() == '\n') {
            index += 1
        }

        return true
    }
}
